在D04 - 設計資料模型我們有提到過領域驅動開發
以及資料模型設計
。在設計的時候我們處於一個理想化的世界,不必太在乎具體怎麼實作,只要把使用者需求映射成資料模型與工作流程就可以了,所以一開始設計的時候通常只會考慮到綠色區塊,可是很快我們就會發現執行上的困難點。
資料傳輸其實是有格式限制的,它無法直接把每個執行環境的型別轉換為可序列化的二元碼。舉例來說 literal type
在 JSON
裡面就是 string
,function
無法被序列化,如果我們直接把 BigInt
或是 recursive object
放入 JSON.stringify
更是會直接報錯。
該如何解決呢? 其實就是要自己做轉換啦 ! 類似這樣的模式其實非常常見,我們可以把它做歸納,負責執行這類任務的模型就叫做資料傳輸模型 (Data Transfer Object, DTO)。它會介於領域模型與外部IO之間,它雖然無法充分表示與需求相關的模型,卻可以直接序列化後進行傳輸。
接下來我們就以 User
和 UserDocument
為範例來說明如何做領域模型到傳輸模型之間的轉換。
我喜歡把轉換的邏輯寫在傳輸模型中,這是參考
Uncle Bob
的clear architecture
的概念,內部核心商業邏輯不應該依賴外部模組,所以User
不應該包含如何轉換成UserDocument
的邏輯。
來源
從領域模型到傳輸模型很簡單可以直接轉。
const mongooseSchema: mongoose.Schema = new mongoose.Schema<UserDocument>({
role: String,
name: String,
})
const of =
(name: string) =>
(role: string): UserDocument => ({
role,
name,
})
const fromUser = (user: User) =>
pipe(
M.value(user._tag),
// 相當於 (user._tag)=>of(user.name)(user._tag)
M.when('Administrator', of(user.name)),
M.when('Participant', of(user.name)),
M.exhaustive
)
不過從傳輸模型轉回領域模型就可能出錯了,這是因為 UserDocument.role
的型別是 string
,它有可能轉不回 User._tag: Administrator | Participant
,所以我們要做錯誤處理,像是以下這樣
const onToUserError = () =>
Either.left(
TransformError.of({
from: 'UserDocument',
to: 'User',
message: 'role should be Administrator or Participant',
})
)
const toUser = (doc: UserDocument): Either.Either<TransformError, User> =>
pipe(
M.value(doc.role),
M.when('Administrator', () => Either.right(Administrator.of(doc.name))),
M.when('Participant', () => Either.right(Participant.of(doc.name))),
M.orElse(onToUserError)
)
今天有講到 Clean Archtecture
,其中方便抽換外部模組
大概算是這個架構的特長了,可惜的是我們大部分看到的 Clean Archtecture
的實作範例都是基於 Class
的,而且常常需要撰寫很多實際上用不到的抽象類別。
接下來兩天會跟大家分享如何基於 FP 達到同樣的目標。